From 8c9c920325fa10440a96736ba58ec647a0365e22 Mon Sep 17 00:00:00 2001
From: "Avi Halachmi (:avih)" <avihpit@yahoo.com>
Date: Sat, 18 Apr 2020 13:56:11 +0300
Subject: [PATCH] application-sync: support Synchronized-Updates

See https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec

In a nutshell: allow an application to suspend drawing until it has
completed some output - so that the terminal will not flicker/tear by
rendering partial content. If the end-of-suspension sequence doesn't
arrive, the terminal bails out after a timeout (default: 200 ms).

The feature is supported and pioneered by iTerm2. There are probably
very few other terminals or applications which support this feature
currently.

One notable application which does support it is tmux (master as of
2020-04-18) - where cursor flicker is completely avoided when a pane
has new content. E.g. run in one pane: `while :; do cat x.c; done'
while the cursor is at another pane.

The terminfo string `Sync' added to `st.info' is also a tmux extension
which tmux detects automatically when `st.info` is installed.

Notes:

- Draw-suspension begins on BSU sequence (Begin-Synchronized-Update),
  and ends on ESU sequence (End-Synchronized-Update).

- BSU, ESU are "\033P=1s\033\\", "\033P=2s\033\\" respectively (DCS).

- SU doesn't support nesting - BSU begins or extends, ESU always ends.

- ESU without BSU is ignored.

- BSU after BSU extends (resets the timeout), so an application could
  send BSU in a loop and keep drawing suspended - exactly like it can
  not-draw anything in a loop. But as soon as it exits/aborted then
  drawing is resumed according to the timeout even without ESU.

- This implementation focuses on ESU and doesn't really care about BSU
  in the sense that it tries hard to draw exactly once ESU arrives (if
  it's not too soon after the last draw - according to minlatency),
  and doesn't try to draw the content just up-to BSU. These two sides
  complement eachother - not-drawing on BSU increases the chance that
  ESU is not too soon after the last draw. This approach was chosen
  because the application's main focus is that ESU indicates to the
  terminal that the content is now ready - and that's when we try to
  draw.
---
 config.def.h |  6 ++++++
 st.c         | 48 ++++++++++++++++++++++++++++++++++++++++++++++--
 st.info      |  1 +
 x.c          | 22 +++++++++++++++++++---
 4 files changed, 72 insertions(+), 5 deletions(-)

diff --git a/config.def.h b/config.def.h
index 6f05dce..80d768e 100644
--- a/config.def.h
+++ b/config.def.h
@@ -56,6 +56,12 @@ int allowwindowops = 0;
 static double minlatency = 8;
 static double maxlatency = 33;
 
+/*
+ * Synchronized-Update timeout in ms
+ * https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
+ */
+static uint su_timeout = 200;
+
 /*
  * blinking timeout (set to 0 to disable blinking) for the terminal blinking
  * attribute.
diff --git a/st.c b/st.c
index 76b7e0d..0582e77 100644
--- a/st.c
+++ b/st.c
@@ -231,6 +231,33 @@ static uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
 static Rune utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
 static Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
 
+#include <time.h>
+static int su = 0;
+struct timespec sutv;
+
+static void
+tsync_begin()
+{
+	clock_gettime(CLOCK_MONOTONIC, &sutv);
+	su = 1;
+}
+
+static void
+tsync_end()
+{
+	su = 0;
+}
+
+int
+tinsync(uint timeout)
+{
+	struct timespec now;
+	if (su && !clock_gettime(CLOCK_MONOTONIC, &now)
+	       && TIMEDIFF(now, sutv) >= timeout)
+		su = 0;
+	return su;
+}
+
 ssize_t
 xwrite(int fd, const char *s, size_t len)
 {
@@ -818,6 +845,9 @@ ttynew(char *line, char *cmd, char *out, char **args)
 	return cmdfd;
 }
 
+static int twrite_aborted = 0;
+int ttyread_pending() { return twrite_aborted; }
+
 size_t
 ttyread(void)
 {
@@ -826,7 +856,7 @@ ttyread(void)
 	int ret, written;
 
 	/* append read bytes to unprocessed bytes */
-	ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
+	ret = twrite_aborted ? 1 : read(cmdfd, buf+buflen, LEN(buf)-buflen);
 
 	switch (ret) {
 	case 0:
@@ -834,7 +864,7 @@ ttyread(void)
 	case -1:
 		die("couldn't read from shell: %s\n", strerror(errno));
 	default:
-		buflen += ret;
+		buflen += twrite_aborted ? 0 : ret;
 		written = twrite(buf, buflen, 0);
 		buflen -= written;
 		/* keep any incomplete UTF-8 byte sequence for the next call */
@@ -994,6 +1024,7 @@ tsetdirtattr(int attr)
 void
 tfulldirt(void)
 {
+	tsync_end();
 	tsetdirt(0, term.row-1);
 }
 
@@ -1895,6 +1926,12 @@ strhandle(void)
 		xsettitle(strescseq.args[0]);
 		return;
 	case 'P': /* DCS -- Device Control String */
+		/* https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */
+		if (strstr(strescseq.buf, "=1s") == strescseq.buf)
+			tsync_begin();  /* BSU */
+		else if (strstr(strescseq.buf, "=2s") == strescseq.buf)
+			tsync_end();  /* ESU */
+		return;
 	case '_': /* APC -- Application Program Command */
 	case '^': /* PM -- Privacy Message */
 		return;
@@ -2436,6 +2473,9 @@ twrite(const char *buf, int buflen, int show_ctrl)
 	Rune u;
 	int n;
 
+	int su0 = su;
+	twrite_aborted = 0;
+
 	for (n = 0; n < buflen; n += charsize) {
 		if (IS_SET(MODE_UTF8)) {
 			/* process a complete utf8 char */
@@ -2446,6 +2486,10 @@ twrite(const char *buf, int buflen, int show_ctrl)
 			u = buf[n] & 0xFF;
 			charsize = 1;
 		}
+		if (su0 && !su) {
+			twrite_aborted = 1;
+			break;  // ESU - allow rendering before a new BSU
+		}
 		if (show_ctrl && ISCONTROL(u)) {
 			if (u & 0x80) {
 				u &= 0x7f;
diff --git a/st.info b/st.info
index 8201ad6..b32b446 100644
--- a/st.info
+++ b/st.info
@@ -191,6 +191,7 @@ st-mono| simpleterm monocolor,
 	Ms=\E]52;%p1%s;%p2%s\007,
 	Se=\E[2 q,
 	Ss=\E[%p1%d q,
+	Sync=\EP=%p1%ds\E\\,
 
 st| simpleterm,
 	use=st-mono,
diff --git a/x.c b/x.c
index 210f184..27ff4e2 100644
--- a/x.c
+++ b/x.c
@@ -1861,6 +1861,9 @@ resize(XEvent *e)
 	cresize(e->xconfigure.width, e->xconfigure.height);
 }
 
+int tinsync(uint);
+int ttyread_pending();
+
 void
 run(void)
 {
@@ -1895,7 +1898,7 @@ run(void)
 		FD_SET(ttyfd, &rfd);
 		FD_SET(xfd, &rfd);
 
-		if (XPending(xw.dpy))
+		if (XPending(xw.dpy) || ttyread_pending())
 			timeout = 0;  /* existing events might not set xfd */
 
 		seltv.tv_sec = timeout / 1E3;
@@ -1909,7 +1912,8 @@ run(void)
 		}
 		clock_gettime(CLOCK_MONOTONIC, &now);
 
-		if (FD_ISSET(ttyfd, &rfd))
+		int ttyin = FD_ISSET(ttyfd, &rfd) || ttyread_pending();
+		if (ttyin)
 			ttyread();
 
 		xev = 0;
@@ -1933,7 +1937,7 @@ run(void)
 		 * maximum latency intervals during `cat huge.txt`, and perfect
 		 * sync with periodic updates from animations/key-repeats/etc.
 		 */
-		if (FD_ISSET(ttyfd, &rfd) || xev) {
+		if (ttyin || xev) {
 			if (!drawing) {
 				trigger = now;
 				drawing = 1;
@@ -1944,6 +1948,18 @@ run(void)
 				continue;  /* we have time, try to find idle */
 		}
 
+		if (tinsync(su_timeout)) {
+			/*
+			 * on synchronized-update draw-suspension: don't reset
+			 * drawing so that we draw ASAP once we can (just after
+			 * ESU). it won't be too soon because we already can
+			 * draw now but we skip. we set timeout > 0 to draw on
+			 * SU-timeout even without new content.
+			 */
+			timeout = minlatency;
+			continue;
+		}
+
 		/* idle detected or maxlatency exhausted -> draw */
 		timeout = -1;
 		if (blinktimeout && tattrset(ATTR_BLINK)) {

base-commit: b27a383a3acc7decf00e6e889fca265430b5d329
-- 
2.17.1

